/********************************************************************************************
 *	MassTest.cpp																			*
 *																							*
 *	Description: Helper program to run AutoTester.exe for all Source and SQueries files		*
 *	in an automated fashion. Make sure that the same number of Source and SQueries files	*
 *	exist in the working directory of MassTest.exe, as well as the autotester itself.		*
 *	The output is n number of xml files (named out<suffix>.xml), based on the number of		*
 *	matching Source and SQueries files found.												*
 ********************************************************************************************/

// -------------------------------
// Include necessary header files
// -------------------------------
#include <windows.h>
#include <string>
#include <stdio.h>
#include <iostream>
#include <vector>
#include <cstring>
#include <sstream>
#include <assert.h>
#include <fstream>

using namespace std;

// -----------------
// Global Constants
// -----------------
const wstring SOURCE_PREFIX = L"Source";
const wstring QUERIES_PREFIX = L"SQueries";
const wstring OUTPUT_PREFIX = L"out";
const wstring INPUT_FORMAT = L".txt";
const wstring OUTPUT_FORMAT = L".xml";
const wstring AUTOTESTER_NAME = L"AutoTester.exe";
const float TIME_LIMIT = 5000;
const int POS_TIME_TAKEN = 12;

// --------------------
// Function Prototypes
// --------------------
string getExePath(void);
void runAutoTesters(vector<wstring> sources, vector<wstring> queries, vector<wstring> outputs, wstring execPath);
bool hasValidPairs(vector<wstring> sources, vector<wstring> queries, bool isAutoTesterFound);
void printResults(vector<wstring> outputs, vector<wstring> queries);
bool isPassed(string line);
bool isTimeout(string line);
double getTime(string line);
wstring initVectors(string folder, vector<wstring> &sources, vector<wstring> &queries, 
	vector<wstring> &outputs, bool &isAutoTesterFound);

// ---------------
// Main procedure
// ---------------
int main(int argc, char* argv[])
{
	// Get current working directory 
	string folder = getExePath();

	// Initialise variables
	vector<wstring> sources;
	vector<wstring> queries;
	vector<wstring> outputs;
	bool isAutoTesterFound = false;

	// Initialize filename vectors
	wstring execPath = initVectors(folder, sources, queries, outputs, isAutoTesterFound);

	// Check for valid source-query pairs
	if(!hasValidPairs(sources, queries, isAutoTesterFound)) {
		cout << "Program will now exit." << endl;
		system("pause");
		exit(1);
	}

	// Run AutoTester as many times as needed
	runAutoTesters(sources, queries, outputs, execPath);

	// Check output and print report
	cout << endl;
	printResults(outputs, queries);

	system("pause");
}

// ------------------------------------------------------------------------
// Helper initialise string vectors containing source and query file names
// ------------------------------------------------------------------------
wstring initVectors(string folder, vector<wstring> &sources, vector<wstring> &queries, 
	vector<wstring> &outputs, bool &isAutoTesterFound) {
	// Initialize variables
	wstring fileName;
	wstring suffix;
	int erasePos = 0;
	char ch[260];
	char pathOnly[260];
	char DefChar = ' ';

	// Perform formatting of container holding
	// current working directory
	sprintf_s(ch, "%s\\*.*", folder.c_str());
	sprintf_s(pathOnly, "%s\\", folder.c_str());
	int nChars = MultiByteToWideChar(CP_ACP, 0, ch, -1, NULL, 0);
	const WCHAR *search_path = new WCHAR[nChars];
	const WCHAR *only_path = new WCHAR[nChars];
	MultiByteToWideChar(CP_ACP, 0, ch, -1, (LPWSTR)search_path, nChars);
	MultiByteToWideChar(CP_ACP, 0, pathOnly, -1, (LPWSTR)only_path, nChars);
	wstring execPath(only_path);

	// Start to scan all files in the current working directory
	WIN32_FIND_DATA fd;

	cout << "Current working directory: " << folder << endl << endl;
	HANDLE hFind = ::FindFirstFile(search_path, &fd); 

	if(hFind != INVALID_HANDLE_VALUE) 
	{ 
		do 
		{ 
			// Read all (real) files in current folder
			// and take note of relevant files.
			if(! (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ) 
			{
				WideCharToMultiByte(CP_ACP, 0, fd.cFileName, -1, ch, 260, &DefChar, NULL);
				fileName = wstring(fd.cFileName);
				
				if (fileName.find(SOURCE_PREFIX) == 0 && fileName.find(INPUT_FORMAT) == fileName.length() - INPUT_FORMAT.length()) {
					
					// Format suffix and output filename
					suffix = wstring(fd.cFileName);
					suffix.erase(0, SOURCE_PREFIX.length());
					erasePos = suffix.find(INPUT_FORMAT);
					suffix.erase(erasePos, INPUT_FORMAT.length());
					suffix.append(OUTPUT_FORMAT);
					suffix = OUTPUT_PREFIX + suffix;

					sources.push_back(fileName);
					outputs.push_back(suffix);
				}
				else if (fileName.find(QUERIES_PREFIX) == 0 && fileName.find(INPUT_FORMAT) == fileName.length() - INPUT_FORMAT.length()) {
					queries.push_back(fileName);
				}
				else if (fileName.find(AUTOTESTER_NAME) == 0) {
					execPath += wstring(fd.cFileName);
					isAutoTesterFound = true;
				}
			}
		}while(::FindNextFile(hFind, &fd)); 
		::FindClose(hFind); 
	} 

	return execPath;
}

// -------------------------------------------------
// Helper function to get current working directory
// -------------------------------------------------
string getExePath(void) {
	wchar_t buffer[MAX_PATH];
	GetModuleFileName( NULL, buffer, MAX_PATH );

	char ch[260];
	char DefChar = ' ';
	WideCharToMultiByte(CP_ACP, 0, buffer, -1, ch, 260, &DefChar, NULL);
	string::size_type pos = string( ch ).find_last_of( "\\/" );
	return string( ch ).substr( 0, pos);
}

// ------------------------------------------------------------------------
// Helper function to run AutoTester.exe based on valid source-query pairs
// ------------------------------------------------------------------------
void runAutoTesters(vector<wstring> sources, vector<wstring> queries, vector<wstring> outputs, wstring execPath) {
	// Initialize some values in preparation for process creation
	STARTUPINFO si;
	PROCESS_INFORMATION pi;
	ZeroMemory( &si, sizeof(si) );
    si.cb = sizeof(si);
    ZeroMemory( &pi, sizeof(pi) );

	// Execute AutoTester.exe with necessary arguments for each source/query pair found
	// [Assume sorted]
	for(unsigned int i = 0; i < sources.size(); i++) {

		// Format arguments 
		execPath = execPath;
		wstring args = L" \"" + sources.at(i) + L"\" \"" + queries.at(i) + L"\" \"" + outputs.at(i) + L"\"";
		LPWSTR wArgs = const_cast<LPWSTR>(args.c_str());

		// Print to screen for confirmation
		cout << "Currently running the following program:" << endl;
		wcout << execPath << args << endl;

		// Start to create process with appropirate arguments
		int res = CreateProcess(
			execPath.c_str(),		//  pointer to name of executable module  
			wArgs,		//  pointer to command line string  
			NULL,		//  pointer to process security attributes  
			NULL,		//  pointer to thread security attributes  
			TRUE,		//  handle inheritance flag  
			0,			//  creation flags  
			NULL,		//  pointer to new environment block  
			NULL,		//  pointer to current directory name  
			&si,		//  pointer to STARTUPINFO  
			&pi			//  pointer to PROCESS_INFORMATION  
			);

		// Check if failed to create process
		if (!res) {
			cout << "Failed to open one or more files. System will now exit" << endl;
			system("pause");
			exit(1);
		}
		
		// Wait for entire run of one AutoTester.exe process to finish
		WaitForSingleObject(pi.hProcess, INFINITE);

		// Close process completely, and move on to next run
		CloseHandle(pi.hProcess);
		CloseHandle(pi.hThread);
	}
}

// ---------------------------------------------------------------------------------
// Perform basic checks on whether autotester and matching source files are present
// (assumes matching source/query files if same number of each are encountered)
// ---------------------------------------------------------------------------------
bool hasValidPairs(vector<wstring> sources, vector<wstring> queries, bool isAutoTesterFound) {
	if (!isAutoTesterFound) {
		cout << "AutoTester.exe not found. Please check again." << endl;
		return false;
	}
	else if (sources.size() != queries.size()) {
		cout << "Number of source files and query files do not match. Please check again." << endl;
		return false;
	}
	else if (sources.size() == 0 && queries.size() == 0) {
		cout << "No source nor query files found. Please check again." << endl;
		return false;
	}
	
	return true;
}

// --------------------------------------------------------------------
// Helper function to run check through output files and print results
// --------------------------------------------------------------------
void printResults(vector<wstring> outputs, vector<wstring> queries) {
	ifstream resultFile;
	double time;
	bool hasFails = false;
	unsigned int indivSuccess, indivTimeouts, indivQueries, indivExceptions, currentQuery, maxTimeQuery;
	unsigned int totalFiles = outputs.size();
	string line, tempString, detail;
	unsigned int correctFiles = 0;
	unsigned int totalQueries = 0;
	unsigned int totalSuccess = 0;
	unsigned int totalTimeouts = 0;
	unsigned int totalExceptions = 0;
	unsigned int maxTime = 0;
	vector<unsigned int> hasFailures;
	vector<string> failureDetails;


	// Loop for each output file
	for(unsigned int i = 0; i < totalFiles; i++) {
		resultFile.open(outputs.at(i));
		indivSuccess = 0;
		indivQueries = 0;
		indivTimeouts = 0;
		indivExceptions = 0;
		currentQuery = 0;
		maxTimeQuery = 0;
		maxTime = 0;
		detail = "";

		tempString.assign(outputs.at(i).begin(), outputs.at(i).end());
		cout << "Checking <" << tempString << ">... " ;
		
		while(getline(resultFile, line)) {
			// Search for keyword
			if(line.find("<query>") == string::npos) {
				continue;
			}

			currentQuery++;

			// If found, skip until important line
			getline(resultFile, line);	//id
			getline(resultFile, line);	//stuans or timeout or exception

			// Check if timeout
			if (isTimeout(line)) {
				indivTimeouts++;
				tempString.assign(queries.at(i).begin(), queries.at(i).end());
				ostringstream ss;
				ss << "  Timeout at Query #" << currentQuery << " of <" << tempString << ">\n";
				detail.append(ss.str());
				indivQueries++;
				continue;
			}
			else if (line.find("<exception/>") != string::npos) {
				indivExceptions++;
				tempString.assign(queries.at(i).begin(), queries.at(i).end());
				ostringstream ss;
				ss << "  Exception at Query #" << currentQuery << " of <" << tempString << ">\n";
				detail.append(ss.str());
				indivQueries++;
				continue;
			}

			getline(resultFile, line);	//correct ans
			getline(resultFile, line);	//time taken

			// Get time taken to complete
			time = getTime(line);
			if (time > maxTime) {
				maxTime = time;
				maxTimeQuery = currentQuery;
			}
			
			// Check if query passes test
			getline(resultFile, line);
			
			if(isPassed(line) && time < TIME_LIMIT) {
				indivSuccess++;
			}
			/*else if (/*isPassed(line) && time > TIME_LIMIT) {
				indivTimeouts++;
				tempString.assign(queries.at(i).begin(), queries.at(i).end());
				ostringstream ss;
				ss << "  Timeout at Query #" << currentQuery << " of <" << tempString << ">\n";
				detail.append(ss.str());
			}*/
			else {
				assert(!isPassed(line));
				tempString.assign(queries.at(i).begin(), queries.at(i).end());
				ostringstream ss;
				ss << "  Failure at Query #" << currentQuery << " of <" << tempString << ">\n";
				detail.append(ss.str());
			}
			indivQueries++;
		}

		//  Mark down which files have queries which failed
		if (indivQueries == indivSuccess) {
			assert(indivTimeouts == 0);
			correctFiles++;
			hasFailures.push_back(0);
		}
		else {
			hasFailures.push_back(1);
			hasFails = true;
		}
		
		// Update overall statistics
		failureDetails.push_back(detail);
		totalTimeouts += indivTimeouts;
		totalExceptions += indivExceptions;
		totalSuccess += indivSuccess;
		totalQueries += indivQueries;

		// Print individual output file statistics
		cout << indivSuccess << "/" << indivQueries << " passed!\n    [Max time taken: " << maxTime << "ms, at Query #" << maxTimeQuery << "]\n" << endl;
		resultFile.close();
	}
	
	assert(failureDetails.size() == hasFailures.size() 
		&& hasFailures.size() == queries.size()
		&& queries.size() == outputs.size());
	
	// Show any detected failures
	cout << endl << "Failures detected: ";

	if (hasFails) {
		cout << endl;
		for (unsigned int i = 0; i < hasFailures.size(); i++) {
			if (hasFailures.at(i) == 1) {
				cout << failureDetails.at(i);
			}
		}
	}
	else {
		cout << "None!" << endl;
	}

	// Print overall statistics
	cout << endl << correctFiles << "/" << totalFiles << " files passed, with a total of "
		<< totalSuccess << "/" << totalQueries << " queries passed." << endl << endl;

	return;
}

// --------------------------------------------------
// Helper function to extract time_taken from a line
// --------------------------------------------------
double getTime(string line) {
	int pos;
	line.erase(0, POS_TIME_TAKEN);
	pos = line.find("</time_taken>");
	line.erase(pos, POS_TIME_TAKEN + 1);

	return atof(line.c_str());
}

// ---------------------------------------------------
// Helper function to run check if a query has passed
// ---------------------------------------------------
bool isPassed(string line) {
	if (line.find("<passed/>") != string::npos) {
		return true;
	}
	else if (line.find("<failed>") != string::npos) {
		return false;
	}
	else {
		return false;
	}
}

// ------------------------------------------------------
// Helper function to run check if a query has timed out
// ------------------------------------------------------
bool isTimeout(string line) {
	if (line.find("<timeout/>") != string::npos) {
		return true;
	}
	return false;
}